﻿// INGENIOUS FRAMEWORK - Semantic Web Development Simplified
// Copyright (C) 2012 Linked Data Tools

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;

using Ingenious.Graph;
using Ingenious.Core;
using Ingenious.Utility;

namespace Ingenious.GraphStore
{
    #region Supporting Classes
    public delegate void PropertyLoadedDelegate<TTripleId>(string propertyName, GraphScope<TTripleId> transactionScope, Triple<TTripleId> triple);
    public delegate void PropertySentDelegate<TTripleId>(string propertyName, GraphScope<TTripleId> transactionScope, Triple<TTripleId> triple);

    /// <summary>
    /// Creates a periodically purged in-memory weak reference cache of nodes
    /// </summary>
    /// <typeparam name="TTripleId"></typeparam>
    public class WeakTripleCache<TTripleId> : IDisposable
    {
        #region Object Lifetime
        public WeakTripleCache()
        {

        }

        public void Dispose()
        {
            
        }
        #endregion
    }
    #endregion
    
    /// <summary>
    /// The base model provider for 'native' persisted models
    /// </summary>
    public abstract class GraphStoreBase<TTripleId> : IDisposable
    {
        #region Constants
        private const int DefaultMaxThreads = 5;
        #endregion

        // Note: This is actually the only in-memory store of loaded items in the framework
        private readonly Dictionary<string, Dictionary<TTripleId, WeakReference>> m_weakItemsCacheByGraphName = new Dictionary<string, Dictionary<TTripleId, WeakReference>>();

        private readonly Dictionary<string, Graph<TTripleId>> m_graphsByName = new Dictionary<string, Graph<TTripleId>>();
        private readonly DispatcherSynchronizationContext m_synchronizationContext = new DispatcherSynchronizationContext(DefaultMaxThreads);

        private readonly GraphProxy<TTripleId> m_graphProxy;
        private readonly GraphStoreConnectionInfoBase m_graphStoreConnectionInfo;

        private readonly GraphQueryProviderBase m_defaultQueryProvider;

        #region Object Lifetime
        public GraphStoreBase(GraphProxy<TTripleId> graphProxy, GraphStoreConnectionInfoBase graphStoreConnectionInfo)
        {
            if (graphProxy == null)
            {
                throw new ArgumentNullException("modelProxy");
            }

            if (graphStoreConnectionInfo == null)
            {
                throw new ArgumentNullException("modelStoreConnection");
            }

            m_graphProxy = graphProxy;
            m_graphStoreConnectionInfo = graphStoreConnectionInfo;

            m_defaultQueryProvider = CreateDefaultQueryProvider();
        }

        public virtual void Dispose()
        {
            if (m_weakItemsCacheByGraphName != null)
            {
                foreach (Dictionary<TTripleId, WeakReference> weakItemsCache in m_weakItemsCacheByGraphName.Values)
                {
                    weakItemsCache.Clear();
                }

                m_weakItemsCacheByGraphName.Clear();
            }

            m_synchronizationContext.Dispose();
        }
        #endregion

        // Note: Loads a list of property names and their associated triple IDs. Does not load the values
        // referenced by a property
        protected abstract IList<Tuple<string, NodePropertyInfo<TTripleId>>> InternalGetPropertyInfosForSubjectUri(Graph<TTripleId> graph, Uri subjectUri);

        public IList<Tuple<string, NodePropertyInfo<TTripleId>>> GetPropertyInfosForSubjectUri(Graph<TTripleId> graph, Uri subjectUri)
        {
            return InternalGetPropertyInfosForSubjectUri(graph, subjectUri);
        }

        // All delay loaded triple items have a unique ID determined by
        // the data storage provider
        public bool IsLoaded(string graphName, TTripleId tripleId)
        {
            return m_weakItemsCacheByGraphName.ContainsKey(graphName) && m_weakItemsCacheByGraphName[graphName].ContainsKey(tripleId) && m_weakItemsCacheByGraphName[graphName][tripleId].Target != null;
        }

        protected abstract Triple<TTripleId> InternalLoadTripleById(string graphName, TTripleId tripleId);

        public Triple<TTripleId> GetTripleById(GraphScope<TTripleId> graphScope, string graphName, TTripleId tripleId, string propertyName, FetchMode fetchMode, PropertyLoadedDelegate<TTripleId> itemLoadedCallback)
        {
            if (graphScope == null)
            {
                throw new ArgumentNullException("transactionScope");
            }

            if (!IsLoaded(graphName, tripleId))
            {
                Triple<TTripleId> triple = null;

                // Load from the data store (e.g. memory, a database) either
                // synchronously or asynchronously depending on the fetch mode
                if (fetchMode == FetchMode.Synchronous)
                {                    
                    // Fetch the item from the store in the sync context thread
                    // and block until completion
                    m_synchronizationContext.Send( (o) =>
                    {
                        triple = InternalLoadTripleById(graphName, tripleId);

                        Dictionary<TTripleId, WeakReference> weakItemsCache = null;

                        if (!m_weakItemsCacheByGraphName.TryGetValue(graphName, out weakItemsCache))
                        {
                            weakItemsCache = new Dictionary<TTripleId, WeakReference>();
                            m_weakItemsCacheByGraphName[graphName] = weakItemsCache;
                        }

                        // Store in the weak reference cache
                        weakItemsCache[tripleId] = new WeakReference(triple);
                    }, null);

                    return triple;
                }
                else
                {                    
                    // Farm off to our provider sync context and raise the callback
                    // in the provider sync context thread when it completes
                    m_synchronizationContext.Post((o) =>
                    {
                        triple = InternalLoadTripleById(graphName, tripleId);

                        // Store in the weak reference cache
                        Dictionary<TTripleId, WeakReference> weakItemsCache = null;

                        if (!m_weakItemsCacheByGraphName.TryGetValue(graphName, out weakItemsCache))
                        {
                            weakItemsCache = new Dictionary<TTripleId, WeakReference>();
                            m_weakItemsCacheByGraphName[graphName] = weakItemsCache;
                        }

                        weakItemsCache[tripleId] = new WeakReference(triple);

                        // Callback
                        if (itemLoadedCallback != null)
                        {
                            itemLoadedCallback.Invoke(propertyName, graphScope, triple);
                        }
                    }, null);

                    // Return null - we are in asynchronous mode
                    return null;
                }
            }
            else
            {
                // Already loaded - use the weak reference
                return m_weakItemsCacheByGraphName[graphName][tripleId].Target as Triple<TTripleId>;
            }
        }
        
        #region Model Updates
        protected abstract void SubmitInsertBatchToStore(Graph<TTripleId> graph, IEnumerable<Triple<TTripleId>> insertChanges);
        protected abstract void SubmitUpdateBatchToStore(Graph<TTripleId> graph, IEnumerable<Triple<TTripleId>> updateChanges);
        protected abstract void SubmitRemoveBatchToStore(Graph<TTripleId> graph, IEnumerable<Triple<TTripleId>> removeChanges);

        protected abstract Graph<TTripleId> InternalGetGraph(string graphName);
        protected abstract void InternalRegisterGraph(Graph<TTripleId> graph);

        /// <summary>
        /// Returns an existing graph, or creates a new one
        /// 
        /// You need to create one of these on the client in order to create
        /// a new model branch
        /// </summary>
        public Graph<TTripleId> GetGraph(string graphName)
        {
            Graph<TTripleId> graph = null;

            if (!m_graphsByName.ContainsKey(graphName))
            {
                // Retrieve the named graph, if already present
                graph = InternalGetGraph(graphName);

                if (graph == null)
                {
                    // Not yet present - create and register with the data store
                    graph = new Graph<TTripleId>(GraphProxy, graphName);

                    // Carry out any registration duties required to create the graph on the store
                    InternalRegisterGraph(graph);
                }

                m_graphsByName[graphName] = graph;
            }
            else
            {
                graph = m_graphsByName[graphName];
            }

            // Note: This is the only operation allowed outside of a transaction,
            // because it is only a single atomic operation
            return graph;
        }

        /// <summary>
        /// Issues a batch of insert changes into the target model store
        /// </summary>
        /// <param name="transactionScope">The parent transaction scope</param>
        /// <param name="insertChanges">The insert model changes</param>
        public void InsertChangeBatch(GraphScope<TTripleId> graphScope, IEnumerable<Triple<TTripleId>> insertChanges)
        {
            if (graphScope == null)
            {
                throw new ArgumentNullException("graphScope");
            }

            if (graphScope.Disposed)
            {
                throw new GraphStoreException("Graph scope has been closed");
            }

            // Process the changes to the server-side model
            SubmitInsertBatchToStore(graphScope.Graph, insertChanges);
        }

        public void UpdateChangeBatch(GraphScope<TTripleId> graphScope, IEnumerable<Triple<TTripleId>> updateChanges)
        {
            if (graphScope == null)
            {
                throw new ArgumentNullException("graphScope");
            }

            if (graphScope.Disposed)
            {
                throw new GraphStoreException("Graph scope has been closed");
            }

            // Process the updates to the server-side model
            SubmitUpdateBatchToStore(graphScope.Graph, updateChanges);
        }

        public void RemoveChangeBatch(GraphScope<TTripleId> graphScope, IEnumerable<Triple<TTripleId>> removeChanges)
        {
            if (graphScope == null)
            {
                throw new ArgumentNullException("graphScope");
            }

            if (graphScope.Disposed)
            {
                throw new GraphStoreException("Transaction scope has been closed");
            }

            // Process the changes to the server-side model
            SubmitRemoveBatchToStore(graphScope.Graph, removeChanges);
        }

        // TODO: we purge the dictionary of null/expired weak references every interval to prevent buildup
        public void InsertOrUpdateTriple(GraphScope<TTripleId> graphScope, Triple<TTripleId> triple)
        {
            if (graphScope == null)
            {
                throw new ArgumentNullException("graphScope");
            }

            if (triple == null)
            {
                throw new ArgumentNullException("triple");
            }

            // We don't insert the object/update on the server-side model yet, only on
            // ending the transaction

            // Insert the entity into the client-side model
            if (m_weakItemsCacheByGraphName[graphScope.Graph.Name].ContainsKey(triple.ID))
            {
                // This is an update change - enumerate for server change when the transaction closes
                graphScope.EnumerateChange(new TripleChange<TTripleId>(graphScope.Graph, triple, TripleChangeType.Modify));

                // Update the client-side model immediately
                m_weakItemsCacheByGraphName[graphScope.Graph.Name][triple.ID].Target = triple;
            }
            else
            {
                // This is an insert new change - enumerate for server change when the transaction closes
                graphScope.EnumerateChange(new TripleChange<TTripleId>(graphScope.Graph, triple, TripleChangeType.Add));

                // Update the client-side model immediately
                m_weakItemsCacheByGraphName[graphScope.Graph.Name].Add(triple.ID, new WeakReference(triple));
            }
        }

        public void InsertOrUpdateNode(GraphScope<TTripleId> graphScope, Node<TTripleId> node)
        {
            if (graphScope == null)
            {
                throw new ArgumentNullException("graphScope");
            }

            if (graphScope.Disposed)
            {
                throw new GraphStoreException("Graph scope has been closed");
            }

            // Convert to triples before informing the serializer
            List<Triple<TTripleId>> triples = node.ToTriples();

            foreach (Triple<TTripleId> triple in triples)
            {
                InsertOrUpdateTriple(graphScope, triple);
            }
        }

        #endregion

        #region Abstract Methods
        protected abstract GraphQueryProviderBase CreateDefaultQueryProvider();

        protected virtual GraphQueryProviderBase InternalCreateQueryProvider(Type queryProviderType)
        {
            // By default we do not implement anything other than the default query provider
            throw new NotImplementedException();
        }

        protected GraphQueryProviderBase CreateQueryProvider(Type queryProviderType)
        {
            if (queryProviderType == null)
            {
                throw new ArgumentNullException("queryProviderType");
            }

            if (!typeof(GraphQueryProviderBase).IsAssignableFrom(queryProviderType))
            {
                throw new ArgumentException("queryProviderType needs to be a type of QueryProviderBase", "queryProviderType");
            }

            return InternalCreateQueryProvider(queryProviderType);
        }

        public abstract TTripleId CreateNewTripleId();
        #endregion

        #region Properties
        protected Dictionary<string, Dictionary<TTripleId, WeakReference>> WeakItemsCacheByGraphName
        {
            get { return m_weakItemsCacheByGraphName; }
        }

        public GraphProxy<TTripleId> GraphProxy
        {
            get { return m_graphProxy; }
        }
        #endregion
    }
}
